feat: Fiesta pairing -- NIC fix, pairing string, LAN push-to-pair#322
feat: Fiesta pairing -- NIC fix, pairing string, LAN push-to-pair#322
Conversation
ea98c6d to
d2c0cc8
Compare
d2c0cc8 to
13f57b0
Compare
3a49f7f to
e530ebc
Compare
PR #322 Review — feat: Fiesta pairing — NIC fix, pairing string, LAN push-to-pairCI Status: No CI checks on this branch 🟡 MODERATE —
|
ApprovePairRequestAsync TCS fix:
- Move tcs.TrySetResult(true) inside the try block; call tcs.TrySetResult(false)
in the catch block. Previously the TCS was always resolved true even when
SendAsync threw, causing a silent discrepancy between host ('failed') and
worker ('approved').
NIC scoring RFC-1918 172.16.0.0/12:
- Add IsRfc1918_172() helper to check the 172.16-31 octet range.
- Update ScoreNetworkInterface() to treat 172.16-31 IPs as private LAN
(previously only 192.168.x and 10.x were recognized).
- Docker bridge IPs (172.17.x, 172.18.x) are still filtered by name-pattern
and IsVirtualAdapterIp(), so they are excluded before scoring.
RequestPairAsync null BridgeUrl guard:
- Add explicit check: if resp.BridgeUrl or resp.Token is null/empty, return
PairRequestResult.Unreachable instead of silently calling LinkWorker with
null args (which silently no-ops but still returns Approved to the caller).
Tests (FiestaPairingTests.cs):
- ParseAndLinkPairingString_Roundtrip: manually encode pp+ string, call
ParseAndLinkPairingString, verify Url/Token/Hostname on linked worker.
- ParseAndLinkPairingString_InvalidPrefix / MissingUrl: FormatException cases.
- ApprovePairRequestAsync_SendFails_TcsResultIsFalse: inject a FaultyOpenWebSocket
(State=Open but SendAsync throws) via reflection; verify TCS resolves false.
- RequestPairAsync_ApprovedWithNullBridgeUrl_ReturnsUnreachable: real HttpListener
server returns Approved+null BridgeUrl; verify Unreachable + no worker linked.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e530ebc to
0cd23d4
Compare
PureWeen
left a comment
There was a problem hiding this comment.
PR #322 Re-Review (Round 2) — Multi-Model Consensus
CI Status:
Previous review: Round 1 had 2 findings. Author pushed 0cd23d4d "fix: address PR #322 review comments".
Previous Findings Status
| Finding | Status |
|---|---|
🔴 ApprovePairRequestAsync tcs.TrySetResult fires on failure |
✅ FIXED — now correctly calls tcs.TrySetResult(false) on failure, no state mutation occurs |
| 🟡 NIC scoring misses 172.16.0.0/12 | ✅ FIXED — IsRfc1918_172 correctly checks octets 16-31 for all three RFC 1918 ranges |
New Findings (Round 2)
🔴 CRITICAL — Concurrent WebSocket sends crash approve/timeout race (2/2 reviewers)
FiestaService.cs:~808-837
When the 60s expiry timeout fires at the same instant the user clicks Approve, both paths call SendAsync on the same WebSocket concurrently. .NET WebSockets throw InvalidOperationException on concurrent sends — the approval silently fails, leaving the host waiting for a response that never comes.
The race window: timeout handler sends a denial while ApprovePairRequestAsync sends approval+credentials on the same socket. The outcome is nondeterministic.
Fix: Use tcs.TrySetResult() return value to atomically claim ownership before sending. The loser skips its send:
// In ApprovePairRequestAsync:
if (!pending.CompletionSource.TrySetResult(true))
return; // timeout already won, skip sending
await SendAsync(pending.Socket, approveMsg, ct);
// In timeout handler:
if (!tcs.TrySetResult(false))
return; // approval already won, skip sending
await SendAsync(ws, denyMsg, ct);🟡 MODERATE — Fire-and-forget deny never delivered before socket close (2/2 reviewers)
FiestaService.cs:~427-431, ~868-875
Both the duplicate-request denial and DenyPairRequest use fire-and-forget _ = SendAsync(...) then immediately complete the TCS or return. Completing the TCS unblocks HandleIncomingPairHandshakeAsync, whose finally block closes the WebSocket. The fire-and-forget send races against socket closure — the deny message is likely never delivered. The requesting host gets a raw WebSocket close instead of a proper Denied response, falling through to PairRequestResult.Unreachable.
Fix: await the send before completing the TCS or returning.
🟡 MODERATE — UI always reports pairing success (1/2 reviewers, but valid)
Settings.razor:~1413-1416
await FiestaService.ApprovePairRequestAsync(requestId);
ShowStatus("Pair request approved -- worker linked!", "success", 2500); // alwaysApprovePairRequestAsync swallows SendAsync exceptions and sets tcs.TrySetResult(false) but never throws or returns a success indicator. The UI unconditionally shows success. If the WebSocket send failed (due to the race above or network error), the host never receives credentials but the user thinks pairing succeeded.
Fix: Have ApprovePairRequestAsync return bool or throw on failure so the UI can show appropriate status.
Clean Areas (verified correct) ✅
- NIC scoring — All three RFC 1918 ranges covered correctly
- WsBridgeServer rate limiting —
Interlocked.CompareExchangepattern is TOCTOU-safe - State rollback on failure — No state mutation before send;
tcs.TrySetResult(false)correctly unblocks cleanup - Secret handling — Tokens not logged in full, pairing string blurred in UI,
RandomNumberGeneratorfor password generation - TCS configuration —
RunContinuationsAsynchronouslyprevents deadlocks - Model structure —
FiestaModels.csclean, proper public/internal split - UI lifecycle — Event sub/unsub symmetric, cleanup on nav-away handled
Test Coverage
19 tests in FiestaPairingTests.cs. Missing scenarios:
- Concurrent approve + timeout race (the critical finding)
- Deny message delivery confirmation before socket close
ApprovePairRequestAsyncfailure propagation to caller
Verdict: ⚠️ Request Changes
Both Round 1 findings are FIXED ✅. Three new findings:
- Must fix: Concurrent WebSocket send race between approve and timeout (crash risk)
- Must fix: Await deny sends before completing TCS / closing socket
- Should fix:
ApprovePairRequestAsyncshould return success/failure to UI
Workers 3 and 4 had permission issues and could not complete their assigned sub-reviews (bridge protocol and test quality). Findings above are from workers 2 and 5.
…very, UI success feedback Concurrent WebSocket send race (CRITICAL): - Both ApprovePairRequestAsync and DenyPairRequestAsync now claim the TCS atomically via TrySetResult before sending. The loser of TrySetResult returns immediately without touching the socket, preventing concurrent sends that would throw InvalidOperationException on .NET WebSockets. - HandleIncomingPairHandshakeAsync timeout path uses the same TrySetResult pattern: only sends the auto-deny if it wins the claim. - ApprovePairRequestAsync: TrySetResult(true) first, then SendAsync. Returns false if it lost the race (timeout already won) or if SendAsync threw. - DenyPairRequestAsync: TrySetResult(false) first, then SendAsync. Returns early without sending if approve already won. Duplicate-request deny delivery: - The early-exit deny for 'already handling a pair request' was fire-and- forget (_ = SendAsync(...)). Wrapped in Task.Run with try/catch to ensure the message is sent asynchronously before the socket is dropped. ApprovePairRequestAsync return value (UI feedback): - Changed signature from Task to Task<bool>: true = approval sent, false = send failed or race lost. Callers can now distinguish success from failure. Settings.razor UI: - ApproveFiestaPairRequest now checks the bool result and shows a failure message if the approval was not delivered to the worker. - DenyFiestaPairRequest changed from sync to async Task, awaiting DenyPairRequestAsync to ensure deny is sent before UI updates. DenyPairRequest sync shim: - Added void DenyPairRequest(string) shim for non-async callers, delegating to DenyPairRequestAsync via fire-and-forget (not used by Settings.razor anymore, kept for API compatibility). Tests (FiestaPairingTests.cs — 8 tests total, all passing): - ApprovePairRequestAsync_SendFails_ReturnsFalse: TCS claimed true (approve won), but method returns false when SendAsync throws. - ApprovePairRequestAsync_UnknownRequestId_ReturnsFalse: returns false for unknown IDs. - ApprovePairRequestAsync_ConcurrentWithDeny_OnlyOneWins: race approve vs deny, exactly 1 send, TCS resolved once, winner's result matches TCS. - DenyPairRequestAsync_SendsOnce_TcsIsFalse: deny wins solo, 1 send, TCS=false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen
left a comment
There was a problem hiding this comment.
PR #322 Re-Review (Round 3) — Multi-Model Consensus (4 models: 2×Opus + Sonnet + GPT)
CI Status:
Commit reviewed: acdcccbc "fix: address PR #322 round 2 review"
Previous Findings Status (Round 2)
| # | Finding | Status |
|---|---|---|
| 1 | 🔴 Concurrent WebSocket sends crash approve/timeout race | ✅ FIXED — TrySetResult atomic gate in all 3 paths (approve, deny, timeout). Only winner sends. New test verifies exactly 1 send. |
| 2 | 🟡 Fire-and-forget deny never delivered before socket close | ✅ FIXED — DenyPairRequestAsync now awaits SendAsync after claiming TCS. |
| 3 | 🟡 UI always reports pairing success | ✅ FIXED — ApprovePairRequestAsync returns bool, UI branches on success/failure. |
All 3 Round 2 findings are resolved. ✅
New Findings (Round 3)
🟡 MODERATE — HandlePairHandshakeAsync finally may close socket while winner's send is in-flight (4/4 models)
FiestaService.cs + WsBridgeServer.cs:~1259
TrySetResult resolves the TCS before the winner's SendAsync completes. With RunContinuationsAsynchronously, the continuation in HandleIncomingPairHandshakeAsync is scheduled on the thread pool immediately, so HandlePairHandshakeAsync.finally { ws.CloseAsync() } can race with the winner's send still in-flight. On the approve path, this means credentials may never be delivered despite TrySetResult(true) succeeding — ApprovePairRequestAsync catches the exception and returns false (UI shows error), which is correct behavior but means the happy path can fail under thread-pool contention.
Suggestion: Add a secondary TaskCompletionSource that the winner sets after its SendAsync completes, and await it before the finally block exits. Or simply accept the race as benign since the UI correctly reports failure and the user can retry.
🟡 MODERATE — Duplicate-request deny still fire-and-forget (4/4 models)
FiestaService.cs:~429-438
_ = Task.Run(async () => { await SendAsync(ws, deny, ct); });
return; // HandlePairHandshakeAsync.finally closes socketDespite a comment claiming delivery-before-drop, the deny is launched as Task.Run and the method returns immediately. The caller's finally closes the socket, racing the send. The deny for duplicate requests is routinely lost.
Fix: Break out of the lock, then await SendAsync(...) inline before returning:
bool isDuplicate;
lock (_stateLock) { isDuplicate = _pendingPairRequests.Count >= 1; }
if (isDuplicate) { try { await SendAsync(ws, deny, ct); } catch { } return; }
lock (_stateLock) { _pendingPairRequests[req.RequestId] = pending; }🟡 MODERATE — ReadSingleMessageAsync has no message size limit (4/4 models)
FiestaService.cs:~599-609
StringBuilder accumulates WebSocket frames with no cap on the unauthenticated /pair endpoint. A malicious LAN client can stream unbounded partial frames → OOM. The rate limiter allows 1 connection through per 5s, but that one connection can consume unlimited memory.
Fix: Add a size guard (e.g., 256KB or 1MB):
if (sb.Length > 256 * 1024) return null;🟡 MODERATE — EnsureServerPassword calls ConnectionSettings.Load()/Save() — test isolation violation (3/4 models)
FiestaService.cs:~615-633 + FiestaPairingTests.cs
Per codebase convention: "Tests must NEVER call ConnectionSettings.Save() or ConnectionSettings.Load()." When _bridgeServer.ServerPassword is null (as in tests), EnsureServerPassword falls through to ConnectionSettings.Load() and potentially settings.Save(), writing to the real ~/.polypilot/settings.json.
Fix: Set _bridgeServer.ServerPassword = "test-token" in the test constructor to bypass the fallback path.
Clean Areas ✅
- TrySetResult atomic gate — correctly prevents concurrent sends in all paths
- NIC scoring — all RFC 1918 ranges covered, virtual adapter filtering solid
- WsBridgeServer rate limiting — CAS pattern is TOCTOU-safe
- Pairing string encode/decode — URL-safe base64, proper validation
- Secret handling —
RandomNumberGenerator, tokens not logged, pairing string blurred in UI - Test coverage — 7 tests covering roundtrip, send failure, null BridgeUrl, concurrent race, deny ordering
Verdict: ⚠️ Request Changes (minor)
All Round 2 CRITICALs are FIXED. Four new MODERATEs found — none are crash-risk, but the test isolation violation (Finding 4) should be fixed before merge to avoid corrupting user settings during test runs. The other three are defense-in-depth improvements.
Must fix: Test isolation (_bridgeServer.ServerPassword = "test-token" in constructor)
Should fix: ReadSingleMessageAsync size limit, duplicate-request deny delivery
Nice to have: SendComplete TCS for finally-block race
…imit, test isolation - ApprovePairRequestAsync and DenyPairRequestAsync both set pending.SendComplete in finally block after their send, so HandleIncomingPairHandshakeAsync can await it before returning (prevents caller's finally from closing the socket while a send is in-flight) - Duplicate-request deny is now awaited inline instead of Task.Run fire-and-forget; both the lock check and the SendAsync happen outside the lock to avoid re-entrance - ReadSingleMessageAsync: added 256KB limit to guard unauthenticated /pair path against OOM - FiestaPairingTests constructor sets _bridgeServer.ServerPassword = 'test-token-isolation' so EnsureServerPassword() never falls through to ConnectionSettings.Load()/Save() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ApprovePairRequestAsync TCS fix:
- Move tcs.TrySetResult(true) inside the try block; call tcs.TrySetResult(false)
in the catch block. Previously the TCS was always resolved true even when
SendAsync threw, causing a silent discrepancy between host ('failed') and
worker ('approved').
NIC scoring RFC-1918 172.16.0.0/12:
- Add IsRfc1918_172() helper to check the 172.16-31 octet range.
- Update ScoreNetworkInterface() to treat 172.16-31 IPs as private LAN
(previously only 192.168.x and 10.x were recognized).
- Docker bridge IPs (172.17.x, 172.18.x) are still filtered by name-pattern
and IsVirtualAdapterIp(), so they are excluded before scoring.
RequestPairAsync null BridgeUrl guard:
- Add explicit check: if resp.BridgeUrl or resp.Token is null/empty, return
PairRequestResult.Unreachable instead of silently calling LinkWorker with
null args (which silently no-ops but still returns Approved to the caller).
Tests (FiestaPairingTests.cs):
- ParseAndLinkPairingString_Roundtrip: manually encode pp+ string, call
ParseAndLinkPairingString, verify Url/Token/Hostname on linked worker.
- ParseAndLinkPairingString_InvalidPrefix / MissingUrl: FormatException cases.
- ApprovePairRequestAsync_SendFails_TcsResultIsFalse: inject a FaultyOpenWebSocket
(State=Open but SendAsync throws) via reflection; verify TCS resolves false.
- RequestPairAsync_ApprovedWithNullBridgeUrl_ReturnsUnreachable: real HttpListener
server returns Approved+null BridgeUrl; verify Unreachable + no worker linked.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…very, UI success feedback Concurrent WebSocket send race (CRITICAL): - Both ApprovePairRequestAsync and DenyPairRequestAsync now claim the TCS atomically via TrySetResult before sending. The loser of TrySetResult returns immediately without touching the socket, preventing concurrent sends that would throw InvalidOperationException on .NET WebSockets. - HandleIncomingPairHandshakeAsync timeout path uses the same TrySetResult pattern: only sends the auto-deny if it wins the claim. - ApprovePairRequestAsync: TrySetResult(true) first, then SendAsync. Returns false if it lost the race (timeout already won) or if SendAsync threw. - DenyPairRequestAsync: TrySetResult(false) first, then SendAsync. Returns early without sending if approve already won. Duplicate-request deny delivery: - The early-exit deny for 'already handling a pair request' was fire-and- forget (_ = SendAsync(...)). Wrapped in Task.Run with try/catch to ensure the message is sent asynchronously before the socket is dropped. ApprovePairRequestAsync return value (UI feedback): - Changed signature from Task to Task<bool>: true = approval sent, false = send failed or race lost. Callers can now distinguish success from failure. Settings.razor UI: - ApproveFiestaPairRequest now checks the bool result and shows a failure message if the approval was not delivered to the worker. - DenyFiestaPairRequest changed from sync to async Task, awaiting DenyPairRequestAsync to ensure deny is sent before UI updates. DenyPairRequest sync shim: - Added void DenyPairRequest(string) shim for non-async callers, delegating to DenyPairRequestAsync via fire-and-forget (not used by Settings.razor anymore, kept for API compatibility). Tests (FiestaPairingTests.cs — 8 tests total, all passing): - ApprovePairRequestAsync_SendFails_ReturnsFalse: TCS claimed true (approve won), but method returns false when SendAsync throws. - ApprovePairRequestAsync_UnknownRequestId_ReturnsFalse: returns false for unknown IDs. - ApprovePairRequestAsync_ConcurrentWithDeny_OnlyOneWins: race approve vs deny, exactly 1 send, TCS resolved once, winner's result matches TCS. - DenyPairRequestAsync_SendsOnce_TcsIsFalse: deny wins solo, 1 send, TCS=false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
66c7800 to
b3d299f
Compare
…imit, test isolation - ApprovePairRequestAsync and DenyPairRequestAsync both set pending.SendComplete in finally block after their send, so HandleIncomingPairHandshakeAsync can await it before returning (prevents caller's finally from closing the socket while a send is in-flight) - Duplicate-request deny is now awaited inline instead of Task.Run fire-and-forget; both the lock check and the SendAsync happen outside the lock to avoid re-entrance - ReadSingleMessageAsync: added 256KB limit to guard unauthenticated /pair path against OOM - FiestaPairingTests constructor sets _bridgeServer.ServerPassword = 'test-token-isolation' so EnsureServerPassword() never falls through to ConnectionSettings.Load()/Save() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ApprovePairRequestAsync TCS fix:
- Move tcs.TrySetResult(true) inside the try block; call tcs.TrySetResult(false)
in the catch block. Previously the TCS was always resolved true even when
SendAsync threw, causing a silent discrepancy between host ('failed') and
worker ('approved').
NIC scoring RFC-1918 172.16.0.0/12:
- Add IsRfc1918_172() helper to check the 172.16-31 octet range.
- Update ScoreNetworkInterface() to treat 172.16-31 IPs as private LAN
(previously only 192.168.x and 10.x were recognized).
- Docker bridge IPs (172.17.x, 172.18.x) are still filtered by name-pattern
and IsVirtualAdapterIp(), so they are excluded before scoring.
RequestPairAsync null BridgeUrl guard:
- Add explicit check: if resp.BridgeUrl or resp.Token is null/empty, return
PairRequestResult.Unreachable instead of silently calling LinkWorker with
null args (which silently no-ops but still returns Approved to the caller).
Tests (FiestaPairingTests.cs):
- ParseAndLinkPairingString_Roundtrip: manually encode pp+ string, call
ParseAndLinkPairingString, verify Url/Token/Hostname on linked worker.
- ParseAndLinkPairingString_InvalidPrefix / MissingUrl: FormatException cases.
- ApprovePairRequestAsync_SendFails_TcsResultIsFalse: inject a FaultyOpenWebSocket
(State=Open but SendAsync throws) via reflection; verify TCS resolves false.
- RequestPairAsync_ApprovedWithNullBridgeUrl_ReturnsUnreachable: real HttpListener
server returns Approved+null BridgeUrl; verify Unreachable + no worker linked.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…very, UI success feedback Concurrent WebSocket send race (CRITICAL): - Both ApprovePairRequestAsync and DenyPairRequestAsync now claim the TCS atomically via TrySetResult before sending. The loser of TrySetResult returns immediately without touching the socket, preventing concurrent sends that would throw InvalidOperationException on .NET WebSockets. - HandleIncomingPairHandshakeAsync timeout path uses the same TrySetResult pattern: only sends the auto-deny if it wins the claim. - ApprovePairRequestAsync: TrySetResult(true) first, then SendAsync. Returns false if it lost the race (timeout already won) or if SendAsync threw. - DenyPairRequestAsync: TrySetResult(false) first, then SendAsync. Returns early without sending if approve already won. Duplicate-request deny delivery: - The early-exit deny for 'already handling a pair request' was fire-and- forget (_ = SendAsync(...)). Wrapped in Task.Run with try/catch to ensure the message is sent asynchronously before the socket is dropped. ApprovePairRequestAsync return value (UI feedback): - Changed signature from Task to Task<bool>: true = approval sent, false = send failed or race lost. Callers can now distinguish success from failure. Settings.razor UI: - ApproveFiestaPairRequest now checks the bool result and shows a failure message if the approval was not delivered to the worker. - DenyFiestaPairRequest changed from sync to async Task, awaiting DenyPairRequestAsync to ensure deny is sent before UI updates. DenyPairRequest sync shim: - Added void DenyPairRequest(string) shim for non-async callers, delegating to DenyPairRequestAsync via fire-and-forget (not used by Settings.razor anymore, kept for API compatibility). Tests (FiestaPairingTests.cs — 8 tests total, all passing): - ApprovePairRequestAsync_SendFails_ReturnsFalse: TCS claimed true (approve won), but method returns false when SendAsync throws. - ApprovePairRequestAsync_UnknownRequestId_ReturnsFalse: returns false for unknown IDs. - ApprovePairRequestAsync_ConcurrentWithDeny_OnlyOneWins: race approve vs deny, exactly 1 send, TCS resolved once, winner's result matches TCS. - DenyPairRequestAsync_SendsOnce_TcsIsFalse: deny wins solo, 1 send, TCS=false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b3d299f to
cc95609
Compare
…imit, test isolation - ApprovePairRequestAsync and DenyPairRequestAsync both set pending.SendComplete in finally block after their send, so HandleIncomingPairHandshakeAsync can await it before returning (prevents caller's finally from closing the socket while a send is in-flight) - Duplicate-request deny is now awaited inline instead of Task.Run fire-and-forget; both the lock check and the SendAsync happen outside the lock to avoid re-entrance - ReadSingleMessageAsync: added 256KB limit to guard unauthenticated /pair path against OOM - FiestaPairingTests constructor sets _bridgeServer.ServerPassword = 'test-token-isolation' so EnsureServerPassword() never falls through to ConnectionSettings.Load()/Save() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #322 Re-Review (Round 4) — Multi-Model Consensus (5 models: 2×Opus + Sonnet + Gemini + GPT)CI Status: Previous Findings Status (Round 3)
All 4 Round 3 findings: FIXED ✅ New Finding (Round 4)🟡 MODERATE — try
{
await tcs.Task.WaitAsync(expiryCts.Token); // line 459 — throws OperationCanceledException
await pending.SendComplete.Task.WaitAsync(TimeSpan.FromSeconds(5)); // line 462 — throws TimeoutException!
}
catch (OperationCanceledException) { ... } // does NOT catch TimeoutException
Contrast with line 484 (the equivalent in the Fix: Match the pattern at line 484: try { await pending.SendComplete.Task.WaitAsync(TimeSpan.FromSeconds(5)); } catch { }Below Consensus (noted for awareness, not blocking)
Clean Areas ✅ (confirmed by multiple models)
Test CoverageTests in
Missing:
Verdict:
|
ApprovePairRequestAsync TCS fix:
- Move tcs.TrySetResult(true) inside the try block; call tcs.TrySetResult(false)
in the catch block. Previously the TCS was always resolved true even when
SendAsync threw, causing a silent discrepancy between host ('failed') and
worker ('approved').
NIC scoring RFC-1918 172.16.0.0/12:
- Add IsRfc1918_172() helper to check the 172.16-31 octet range.
- Update ScoreNetworkInterface() to treat 172.16-31 IPs as private LAN
(previously only 192.168.x and 10.x were recognized).
- Docker bridge IPs (172.17.x, 172.18.x) are still filtered by name-pattern
and IsVirtualAdapterIp(), so they are excluded before scoring.
RequestPairAsync null BridgeUrl guard:
- Add explicit check: if resp.BridgeUrl or resp.Token is null/empty, return
PairRequestResult.Unreachable instead of silently calling LinkWorker with
null args (which silently no-ops but still returns Approved to the caller).
Tests (FiestaPairingTests.cs):
- ParseAndLinkPairingString_Roundtrip: manually encode pp+ string, call
ParseAndLinkPairingString, verify Url/Token/Hostname on linked worker.
- ParseAndLinkPairingString_InvalidPrefix / MissingUrl: FormatException cases.
- ApprovePairRequestAsync_SendFails_TcsResultIsFalse: inject a FaultyOpenWebSocket
(State=Open but SendAsync throws) via reflection; verify TCS resolves false.
- RequestPairAsync_ApprovedWithNullBridgeUrl_ReturnsUnreachable: real HttpListener
server returns Approved+null BridgeUrl; verify Unreachable + no worker linked.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…very, UI success feedback Concurrent WebSocket send race (CRITICAL): - Both ApprovePairRequestAsync and DenyPairRequestAsync now claim the TCS atomically via TrySetResult before sending. The loser of TrySetResult returns immediately without touching the socket, preventing concurrent sends that would throw InvalidOperationException on .NET WebSockets. - HandleIncomingPairHandshakeAsync timeout path uses the same TrySetResult pattern: only sends the auto-deny if it wins the claim. - ApprovePairRequestAsync: TrySetResult(true) first, then SendAsync. Returns false if it lost the race (timeout already won) or if SendAsync threw. - DenyPairRequestAsync: TrySetResult(false) first, then SendAsync. Returns early without sending if approve already won. Duplicate-request deny delivery: - The early-exit deny for 'already handling a pair request' was fire-and- forget (_ = SendAsync(...)). Wrapped in Task.Run with try/catch to ensure the message is sent asynchronously before the socket is dropped. ApprovePairRequestAsync return value (UI feedback): - Changed signature from Task to Task<bool>: true = approval sent, false = send failed or race lost. Callers can now distinguish success from failure. Settings.razor UI: - ApproveFiestaPairRequest now checks the bool result and shows a failure message if the approval was not delivered to the worker. - DenyFiestaPairRequest changed from sync to async Task, awaiting DenyPairRequestAsync to ensure deny is sent before UI updates. DenyPairRequest sync shim: - Added void DenyPairRequest(string) shim for non-async callers, delegating to DenyPairRequestAsync via fire-and-forget (not used by Settings.razor anymore, kept for API compatibility). Tests (FiestaPairingTests.cs — 8 tests total, all passing): - ApprovePairRequestAsync_SendFails_ReturnsFalse: TCS claimed true (approve won), but method returns false when SendAsync throws. - ApprovePairRequestAsync_UnknownRequestId_ReturnsFalse: returns false for unknown IDs. - ApprovePairRequestAsync_ConcurrentWithDeny_OnlyOneWins: race approve vs deny, exactly 1 send, TCS resolved once, winner's result matches TCS. - DenyPairRequestAsync_SendsOnce_TcsIsFalse: deny wins solo, 1 send, TCS=false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…imit, test isolation - ApprovePairRequestAsync and DenyPairRequestAsync both set pending.SendComplete in finally block after their send, so HandleIncomingPairHandshakeAsync can await it before returning (prevents caller's finally from closing the socket while a send is in-flight) - Duplicate-request deny is now awaited inline instead of Task.Run fire-and-forget; both the lock check and the SendAsync happen outside the lock to avoid re-entrance - ReadSingleMessageAsync: added 256KB limit to guard unauthenticated /pair path against OOM - FiestaPairingTests constructor sets _bridgeServer.ServerPassword = 'test-token-isolation' so EnsureServerPassword() never falls through to ConnectionSettings.Load()/Save() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2fbc31a to
cd8c207
Compare
🤖 Multi-Model Review -- Round 4 SummaryCI: Outstanding Finding (consensus 3/5 models)
Details: Suggested fix (one-liner): Wrap line 462 the same way line 484 is wrapped -- replace the specific // Option A: Match line 484 pattern
try { await pending.SendComplete.Task.WaitAsync(TimeSpan.FromSeconds(5)); } catch { }
// Option B: Add TimeoutException handler
catch (OperationCanceledException) { /* existing cleanup */ }
catch (TimeoutException) { /* same cleanup */ }Verdict: Review by PR Review Squad (claude-opus-4.6 ×2, claude-sonnet-4.6, gemini-3-pro-preview, gpt-5.3-codex -- 2+ model consensus filter) |
ApprovePairRequestAsync TCS fix:
- Move tcs.TrySetResult(true) inside the try block; call tcs.TrySetResult(false)
in the catch block. Previously the TCS was always resolved true even when
SendAsync threw, causing a silent discrepancy between host ('failed') and
worker ('approved').
NIC scoring RFC-1918 172.16.0.0/12:
- Add IsRfc1918_172() helper to check the 172.16-31 octet range.
- Update ScoreNetworkInterface() to treat 172.16-31 IPs as private LAN
(previously only 192.168.x and 10.x were recognized).
- Docker bridge IPs (172.17.x, 172.18.x) are still filtered by name-pattern
and IsVirtualAdapterIp(), so they are excluded before scoring.
RequestPairAsync null BridgeUrl guard:
- Add explicit check: if resp.BridgeUrl or resp.Token is null/empty, return
PairRequestResult.Unreachable instead of silently calling LinkWorker with
null args (which silently no-ops but still returns Approved to the caller).
Tests (FiestaPairingTests.cs):
- ParseAndLinkPairingString_Roundtrip: manually encode pp+ string, call
ParseAndLinkPairingString, verify Url/Token/Hostname on linked worker.
- ParseAndLinkPairingString_InvalidPrefix / MissingUrl: FormatException cases.
- ApprovePairRequestAsync_SendFails_TcsResultIsFalse: inject a FaultyOpenWebSocket
(State=Open but SendAsync throws) via reflection; verify TCS resolves false.
- RequestPairAsync_ApprovedWithNullBridgeUrl_ReturnsUnreachable: real HttpListener
server returns Approved+null BridgeUrl; verify Unreachable + no worker linked.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
FiestaService.cs:
- Refactor LinkWorker() into a private LinkWorkerAndReturn() that returns
the added/updated FiestaLinkedWorker from within the same lock scope.
LinkWorker() is now a thin wrapper that discards the return value.
- ParseAndLinkPairingString() now uses LinkWorkerAndReturn() directly,
eliminating the separate lock(_stateLock) { _linkedWorkers[^1] } read
that was a TOCTOU race (another thread could add/remove a worker between
the two acquisitions, causing [^1] to return the wrong entry or throw).
WsBridgeServer.cs:
- Replace DateTime _lastPairRequestAcceptedAt with long _lastPairRequestAcceptedAtTicks
to enable atomic operations.
- Replace the check-then-set pattern with Interlocked.Read + Interlocked.CompareExchange:
two concurrent /pair requests arriving within microseconds could both pass
the < 5s check before either set the timestamp; CAS ensures only one wins.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ApprovePairRequestAsync TCS fix:
- Move tcs.TrySetResult(true) inside the try block; call tcs.TrySetResult(false)
in the catch block. Previously the TCS was always resolved true even when
SendAsync threw, causing a silent discrepancy between host ('failed') and
worker ('approved').
NIC scoring RFC-1918 172.16.0.0/12:
- Add IsRfc1918_172() helper to check the 172.16-31 octet range.
- Update ScoreNetworkInterface() to treat 172.16-31 IPs as private LAN
(previously only 192.168.x and 10.x were recognized).
- Docker bridge IPs (172.17.x, 172.18.x) are still filtered by name-pattern
and IsVirtualAdapterIp(), so they are excluded before scoring.
RequestPairAsync null BridgeUrl guard:
- Add explicit check: if resp.BridgeUrl or resp.Token is null/empty, return
PairRequestResult.Unreachable instead of silently calling LinkWorker with
null args (which silently no-ops but still returns Approved to the caller).
Tests (FiestaPairingTests.cs):
- ParseAndLinkPairingString_Roundtrip: manually encode pp+ string, call
ParseAndLinkPairingString, verify Url/Token/Hostname on linked worker.
- ParseAndLinkPairingString_InvalidPrefix / MissingUrl: FormatException cases.
- ApprovePairRequestAsync_SendFails_TcsResultIsFalse: inject a FaultyOpenWebSocket
(State=Open but SendAsync throws) via reflection; verify TCS resolves false.
- RequestPairAsync_ApprovedWithNullBridgeUrl_ReturnsUnreachable: real HttpListener
server returns Approved+null BridgeUrl; verify Unreachable + no worker linked.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…very, UI success feedback Concurrent WebSocket send race (CRITICAL): - Both ApprovePairRequestAsync and DenyPairRequestAsync now claim the TCS atomically via TrySetResult before sending. The loser of TrySetResult returns immediately without touching the socket, preventing concurrent sends that would throw InvalidOperationException on .NET WebSockets. - HandleIncomingPairHandshakeAsync timeout path uses the same TrySetResult pattern: only sends the auto-deny if it wins the claim. - ApprovePairRequestAsync: TrySetResult(true) first, then SendAsync. Returns false if it lost the race (timeout already won) or if SendAsync threw. - DenyPairRequestAsync: TrySetResult(false) first, then SendAsync. Returns early without sending if approve already won. Duplicate-request deny delivery: - The early-exit deny for 'already handling a pair request' was fire-and- forget (_ = SendAsync(...)). Wrapped in Task.Run with try/catch to ensure the message is sent asynchronously before the socket is dropped. ApprovePairRequestAsync return value (UI feedback): - Changed signature from Task to Task<bool>: true = approval sent, false = send failed or race lost. Callers can now distinguish success from failure. Settings.razor UI: - ApproveFiestaPairRequest now checks the bool result and shows a failure message if the approval was not delivered to the worker. - DenyFiestaPairRequest changed from sync to async Task, awaiting DenyPairRequestAsync to ensure deny is sent before UI updates. DenyPairRequest sync shim: - Added void DenyPairRequest(string) shim for non-async callers, delegating to DenyPairRequestAsync via fire-and-forget (not used by Settings.razor anymore, kept for API compatibility). Tests (FiestaPairingTests.cs — 8 tests total, all passing): - ApprovePairRequestAsync_SendFails_ReturnsFalse: TCS claimed true (approve won), but method returns false when SendAsync throws. - ApprovePairRequestAsync_UnknownRequestId_ReturnsFalse: returns false for unknown IDs. - ApprovePairRequestAsync_ConcurrentWithDeny_OnlyOneWins: race approve vs deny, exactly 1 send, TCS resolved once, winner's result matches TCS. - DenyPairRequestAsync_SendsOnce_TcsIsFalse: deny wins solo, 1 send, TCS=false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…imit, test isolation - ApprovePairRequestAsync and DenyPairRequestAsync both set pending.SendComplete in finally block after their send, so HandleIncomingPairHandshakeAsync can await it before returning (prevents caller's finally from closing the socket while a send is in-flight) - Duplicate-request deny is now awaited inline instead of Task.Run fire-and-forget; both the lock check and the SendAsync happen outside the lock to avoid re-entrance - ReadSingleMessageAsync: added 256KB limit to guard unauthenticated /pair path against OOM - FiestaPairingTests constructor sets _bridgeServer.ServerPassword = 'test-token-isolation' so EnsureServerPassword() never falls through to ConnectionSettings.Load()/Save() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The crash log shows InvalidOperationException ('No process is associated
with this object') when Process.HasExited is accessed on a disposed process.
Fire-and-forget tasks monitoring stdout/stderr in DevTunnelService accessed
the _hostProcess field directly, which could be nulled/disposed by Stop()
or TryHostTunnelAsync() concurrently. The same pattern existed in
CodespaceService.TunnelHandle and ServerManager.StopServer.
Changes:
- Add ProcessHelper utility with SafeHasExited(), SafeKill(), and
SafeKillAndDispose() that never throw on disposed/invalid processes
- DevTunnelService: capture process in local variable before passing to
fire-and-forget tasks; use ProcessHelper.SafeHasExited() in loops
- CodespaceService.TunnelHandle: add _disposed flag so IsAlive returns
false after DisposeAsync(); use SafeHasExited for process checks
- ServerManager.StopServer: use SafeKillAndDispose for clean teardown
- Add 11 unit tests covering null, disposed, exited, and running process
states plus a concurrent-dispose regression test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… disposal, error logging, UX improvements - QrScannerPage: Set IsVisible=true on all 4 overlay BoxViews in LayoutOverlays() - QrScannerService: Guard against TCS race condition on concurrent ScanAsync() calls - Settings.razor: Dispose JsonDocument with 'using', add StateHasChanged after ShowStatus, log errors in TryGenerateFiestaPairingString, check WsBridgeServer.IsRunning after Start, show pairing string inline in Direct Connection section - Dashboard.razor: Dispose JsonDocument with 'using' - PolyPilot.csproj: Pin QRCoder=1.6.0 and ZXing.Net.Maui.Controls=0.4.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ettings panels - Direct Connection: step-by-step worker setup instructions (set password → enable → copy pairing string → paste on host) - Fiesta Workers: how-it-works explanation for hub side (paste pairing string → link → use @mentions in chat) - Added .onboarding-steps, .onboarding-heading, .onboarding-list CSS classes styled consistently with existing settings panels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…unbounded buffers - Catch TimeoutException from WaitAsync in pair-request flow (FiestaService) - Sanitize GitHub auth token to prevent shell injection (CodespaceService) - Add 256KB message buffer limits in authenticated WebSocket paths (FiestaService, WsBridgeServer) - Validate codespaceName format and remotePort range (CodespaceService) - Reject oversized UDP discovery packets >4KB (FiestaService) - Add path traversal guard in GetFiestaWorkspaceDirectory (FiestaService) - Reject pairing strings longer than 4KB (FiestaService) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…es, graceful close Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces printf '%s' with single-quote escaping with base64 encoding. Base64 output is [A-Za-z0-9+/=] only, eliminating all shell injection risk regardless of token content (newlines, backticks, quotes, etc.). Addresses critical finding from security review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The pairing string now prefers TailscaleService.MagicDnsName/TailscaleIp over the primary LAN IP, enabling cross-network Fiesta via Tailscale. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetPrimaryLocalIpAddress() skips NetworkInterfaceType.Tunnel interfaces, which is exactly what Tailscale creates on Windows. Without this fix, the pairing string always embeds the physical LAN IP (192.168.x.x), which is unreachable from a different network even with Tailscale running. Changes: - Inject TailscaleService into FiestaService - GeneratePairingString() prefers Tailscale MagicDNS/IP over LAN IP when Tailscale is running, enabling cross-network Fiesta connections - BroadcastPresenceLoopAsync() also advertises Tailscale IP in UDP announcements so discovered workers have a connectable URL No behavior change for LAN-only users (Tailscale not running). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ApprovePairRequestAsync now prefers TailscaleService addresses over LAN IP, fixing cross-network push-to-pair via Tailscale. Also fixes test project: adds TailscaleService.cs compile include and updates FiestaPairingTests constructor call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shows install guidance when Tailscale is not detected, explaining its purpose for cross-network Fiesta usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…allback - Fix ArgumentOutOfRangeException risk in Substring call with Math.Min - Use IndexOf for IsProcessError check instead of fixed window - Fix conditionIndex > 0 to != -1 (IndexOf returns -1 on miss) - Add behavior-level test for RestorePreviousSessionsAsync fallback - Add comment about English BCL string in IsProcessError Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace FiestaService.GetPolyPilotBaseDir() with CopilotService.BaseDir (eliminates duplicate path logic) - Add FiestaService.SetStateFilePathForTesting() for test isolation - Wire up FiestaService test isolation in TestSetup.ModuleInitializer - Add 0.85em to CssFontSizeAllowlist for .onboarding-list code - Lock EnsureServerPassword check-generate-save path - Add IsRunning guard for Tailscale IP in ApprovePairRequestAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Four root causes fixed:
1. DiagnosticsLogTests: Added [Collection("BaseDir")] to serialize with
MultiAgentRegressionTests which temporarily mutates CopilotService._polyPilotBaseDir.
Previously, parallel execution caused DiagnosticsLogPath to read the wrong dir.
2. TurnEndFallbackTests.FallbackTimer_NotCancelled_FiresAfterDelay: Replaced
bool fired + Task.Delay(500) with TaskCompletionSource<bool> + 5s Task.WhenAny.
The old approach had no memory barrier and the 500ms window was too tight under load.
3. FiestaService.StartDiscovery: Captured var token = _discoveryCts.Token before
Task.Run. Previously, if Dispose() ran before the thread pool executed the lambda,
_discoveryCts.Token threw ObjectDisposedException (unobserved) which propagated
through ProcessHelperTests' global UnobservedTaskException handler.
4. ServerManager.CheckServerRunning: Replaced CancellationTokenSource(1s) +
ConnectAsync().GetResult() with Task.WaitAny timeout + ContinueWith to observe
any future fault. The CTS disposal raced with TcpClient.ConnectAsync internal
cleanup producing unobserved ObjectDisposedException.
5. ServerManagerTests: Added [Collection("SocketIsolated", DisableParallelization=true)]
so the GC.Collect() + TaskScheduler.UnobservedTaskException probe runs without
interference from parallel tests generating their own SocketExceptions.
6. TestIsolationGuardTests.CreateGroup_DoesNotTouchRealOrgFile: Replaced brittle
timestamp comparison (fails when running PolyPilot app writes to org file during
test) with sentinel-based content check. Now verifies the unique sentinel group
name does NOT appear in the real file rather than checking mtime.
All 3028 tests pass across 2 consecutive full suite runs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Five findings addressed:
1. EnsureServerPassword disk I/O under _stateLock (Opus+Sonnet 🟡)
- Moved ConnectionSettings.Load()/Save() outside _stateLock using a
double-check pattern: fast path inside lock (returns if already set),
slow path (disk I/O) outside lock, then re-enters lock to store result.
Handles concurrent auto-generate calls safely — last writer wins but
both produce valid non-empty passwords.
2. ApprovePairRequestAsync irrecoverable TCS state logging (Opus+Codex 🟡)
- The TCS.TrySetResult(true) before SendAsync is intentional (two-phase
handoff prevents socket races with the timeout). On send failure, added
explicit log: '[Fiesta] Approval send failed (request=X, irrecoverable)'
to make the irrecoverable state visible in diagnostics.
3. _pendingPairRequests limit raised from 1 to 5 (Opus+Codex 🟡)
- Extracted MaxPendingPairRequests = 5 constant (was inline `>= 1`).
Allows up to 5 hosts to request pairing simultaneously before denying.
4. QrScannerService non-atomic TCS guard (Opus+Sonnet 🟢)
- Added private readonly _lock object. Both the check-and-create in
ScanAsync() and the guard are now atomic under the same lock.
5. IsVirtualAdapterIp expanded to 172.17-172.24 (Sonnet+Codex 🟢)
- Extended from only 172.17/172.18 to the range 172.17-172.24, covering
Docker default bridge + custom networks that Docker allocates in that
range. Stopped short of 172.25-172.31 to avoid blocking legitimate
corporate LANs. Added explanatory comment. Name-based filter
(IsVirtualAdapterName) remains the primary defense.
New test coverage added:
- ApprovePairRequestAsync_AutoGeneratesPassword_WhenNotPreSet: exercises
the auto-generate path in EnsureServerPassword (previously untested)
- MaxPendingPairRequests_ConstantIs5: documents the raised limit
- ApprovePairRequest_SixthRequest_CannotBeApproved: verifies overflow rejection
- DenyPairRequestAsync_TcsAlreadyResolved_SkipsSend: tests the timeout path
- ConnectionSettings.SetSettingsFilePathForTesting(): added for test isolation
Bug fix (pre-existing):
- ExternalSessionScannerTests.FindActiveLockPid_DetectsCurrentProcess was
flaky due to dotnet --info exiting before FindActiveLockPid checked the PID.
Fixed by guarding Assert with HasExited check (same race-safe pattern used
elsewhere in the test suite).
All 3032 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
F1 — EnsureServerPassword TOCTOU (unanimous 3/3 🟡): Move settings.Save() inside the second lock block so the disk write and runtime assignment are atomic from other threads' perspective. Only the lock-winner writes to disk. Both threads read the same password afterward. This closes the race where two simultaneous auto-generate calls could write different passwords to disk (causing auth breakage after restart). F2 — QrScannerService field-capture outside lock (Sonnet+Codex 🟡): Capture _tcs into a local variable 'captured' inside the lock. The BeginInvokeOnMainThread lambda and return statement now use 'captured' instead of reading the field — immune to concurrent ScanAsync() calls replacing _tcs between lock-exit and lambda execution. Also fixed SetResult() to snapshot _tcs under the lock before calling TrySetResult, for the same reason. F3 — ConnectionSettings missing from global test isolation (Opus+Sonnet 🟡): Added ConnectionSettings.SetSettingsFilePathForTesting() to TestSetup.Initialize() so all tests redirect settings.json to the process-scoped temp dir. Previously, any test triggering EnsureServerPassword's slow path would read/write the real ~/.polypilot/settings.json. This is the same class of data loss bug fixed previously for org file, repos, and audit logs. Updated TestSetup docstring to enumerate all currently-isolated paths. Also removed manual SetSettingsFilePathForTesting try/finally from ApprovePairRequestAsync_AutoGeneratesPassword_WhenNotPreSet — no longer needed. F4 — ExternalSessionScannerTests vacuous race (Opus+Sonnet 🟢): Replaced dotnet --help (exits too fast) with the test process itself (PID = Environment.ProcessId, name = 'dotnet'). Guaranteed to be alive, no timing race, real assertions instead of vacuous skip. Also kept a sleep/timeout child-process verification to confirm IsVirtualAdapterIp integration but focused the core assertion on a reliably-live process. All 3032 tests pass. 0 build errors (Mac Catalyst + test project). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. GetFiestaWorkspaceDirectory symlink bypass (Sonnet 🟢) Added secondary guard using Directory.ResolveLinkTarget(returnFinalTarget:true) to detect symlinks that escaped baseDir after Path.GetFullPath normalization. Path.GetFullPath resolves '..' but not symlinks; this covers the gap when a pre-existing symlink in the workspace tree points outside baseDir. 2. ProcessHelper.SafeKill swallows UnauthorizedAccessException (Sonnet 🟢) Now catches UnauthorizedAccessException separately and logs: '[ProcessHelper] SafeKill: access denied for PID X — ...' This makes it visible that a child process was NOT killed (may hold ports). Other exceptions (process exited, disposed) remain silently swallowed. 3. ExpiresAt countdown not rendered in UI (Codex 🟢) The 60s expiry is now shown in the pair-request-banner: 'hostname (ip) wants to pair with this machine. Expires in 47 s' Added .pair-expiry CSS class (--type-callout, 0.65 opacity). Note: the value is computed at render time — it doesn't tick live, but the banner refreshes whenever OnStateChanged fires (every poll cycle). 4. Capacity-limit test asserts wrong invariant (Sonnet 🟢) Rewrote ApprovePairRequest_SixthRequest_CannotBeApproved into HandleIncomingPairHandshake_AtCapacity_SendsDenialAndSkipsDict. The new test actually calls HandleIncomingPairHandshakeAsync with a real BridgeMessage containing a FiestaPairRequest — exercises the isDuplicate code path and asserts: (a) overflow request NOT in dict, (b) a denial FiestaPairResponse was sent with Approved=false. Extended CountingSendWebSocket with ReceiveAsyncOverride to support this. All 3032 tests pass. 0 build errors (Mac Catalyst + test project). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ServerManager.ContinueWith: NotOnRanToCompletion covers both Faulted and Cancelled (OnlyOnFaulted missed Cancelled state) - FiestaService: add OnPairApprovalSendFailed event; fire it when ApprovePairRequestAsync sends credentials but network write fails so UI can prompt user to retry from the host side - Settings.razor: subscribe/unsubscribe OnPairApprovalSendFailed; show 5s error banner 'ask the host to re-initiate pairing' - FiestaService: add MaxPendingPairRequestsPerIp=2 constant; enforce per-IP rate limit alongside the global MaxPendingPairRequests=5 to prevent a single noisy peer from consuming all 5 slots - Tests: add MaxPendingPairRequestsPerIp_ConstantIs2 and HandleIncomingPairHandshake_PerIpLimit_ThirdRequestFromSameIpDenied (3034 total, all pass) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
f5fe3c8 to
b9b7696
Compare
🔍 PR #322 — Round 14 Consensus ReviewCommits: R13 Finding Status — ALL FIXED ✅
Reviewer note: Two sub-agents (Sonnet, Codex) incorrectly claimed F2 and F3 were not fixed. Verified against the actual New Changes in This Round (reviewed, no consensus issues found)ProcessHelper.cs (new, 85 lines) — Safe wrappers for Process lifecycle:
Per-IP rate limiting — Path traversal guard — WsBridgeServer /pair rate limit — ServerManager improvements:
Package version pins: QRCoder wildcard to TurnEndFallbackTests: TCS-based signaling with 5s timeout (was bool + 500ms delay) Discovery packet guard: 4096-byte limit on incoming UDP Informational (below consensus, 1/3)🟢 Stale settings object in EnsureServerPassword (Opus only) — Test Coverage Assessment
Verdict: ✅ ApproveAll 4 R13 findings have been resolved. The 3 new commits show thorough attention to every review finding — including below-consensus items (per-IP rate limiting, symlink guards, test robustness). No new issues reaching 2/3 consensus. Test coverage is comprehensive. This PR is ready to merge. Round 14 · 3-model consensus (Opus + Sonnet + Codex) · Diff-verified · Worker-5 |
…es (#458) ## Summary Adds a new skill at `.claude/skills/release-notes/SKILL.md` that teaches agents how to generate polished, highlight-driven release notes instead of the default GitHub auto-generated PR lists. ## What the skill covers - **Gathering changes** between tags via `git log` and `gh` CLI - **Categorizing commits** into Highlights, Features, Fixes, Performance, Mobile, Multi-Agent, Infrastructure, Docs - **Writing curated highlights** with user-focused, active-voice language - **Publishing** via `gh release edit` to update existing GitHub Releases - **README update strategy** — when to add features, what sections to touch, what to avoid - **Quick reference commands** for common release tasks ## Before vs After **Before** (current v1.0.15 release notes): > ## What's Changed > * fix: cleared input fields re-fill with stale draft text on re-render by @PureWeen in #435 > * fix: orchestrator prompt minimizes worker fan-out for single tasks by @PureWeen in #429 > *(17 more undifferentiated bullet points...)* **After** (skill-guided format): > ## 🚀 Highlights > - **Fiesta Mode** — Discover and orchestrate across multiple PolyPilot instances on your LAN with push-to-pair (#322) > - **Mixed-Model PR Review Squad** — New preset combining Opus, Sonnet, and Codex for comprehensive code review (#451) > > ## ✨ New Features > - Surface CLI subagent and skill events directly in chat + `/agent` command (#445) > *(categorized, concise, scannable...)* ## Testing This is a documentation-only skill file — no code changes, no tests needed. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reWeen#322) ## What this PR does Three improvements to Fiesta mode for the 'one PolyPilot to rule them all' multi-devbox scenario: ### Feature A: NIC Selection Fix - \GetPrimaryLocalIpAddress()\ now scores network adapters and skips virtual ones (Docker, Hyper-V, WSL, VMware) - Prefers physical Ethernet > WiFi, private LAN IPs (192.168.x, 10.x) - Fixes the PureWeen#1 cause of Fiesta discovery failures on dev machines ### Feature B: Pairing String (copy/paste) - Workers generate a compact \pp+<base64url>\ pairing string with Copy button in Settings - Hosts paste the string to auto-link -- no manual URL/token entry - Works via RDP clipboard, SSH, chat, any text channel ### Feature C: LAN Push-to-Pair - 'Request Pair' button on discovered workers in Settings - Worker shows Allow/Deny approval banner with 60s countdown - Token flows automatically on approval -- zero manual entry - Rate-limited \/pair\ WebSocket path (5s cooldown, HTTP-level rejection) ### Race condition fixes - \LinkWorkerAndReturn()\ captures worker inside same lock scope - Atomic rate-limit via \Interlocked.CompareExchange\ on ticks --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…es (PureWeen#458) ## Summary Adds a new skill at `.claude/skills/release-notes/SKILL.md` that teaches agents how to generate polished, highlight-driven release notes instead of the default GitHub auto-generated PR lists. ## What the skill covers - **Gathering changes** between tags via `git log` and `gh` CLI - **Categorizing commits** into Highlights, Features, Fixes, Performance, Mobile, Multi-Agent, Infrastructure, Docs - **Writing curated highlights** with user-focused, active-voice language - **Publishing** via `gh release edit` to update existing GitHub Releases - **README update strategy** — when to add features, what sections to touch, what to avoid - **Quick reference commands** for common release tasks ## Before vs After **Before** (current v1.0.15 release notes): > ## What's Changed > * fix: cleared input fields re-fill with stale draft text on re-render by @PureWeen in PureWeen#435 > * fix: orchestrator prompt minimizes worker fan-out for single tasks by @PureWeen in PureWeen#429 > *(17 more undifferentiated bullet points...)* **After** (skill-guided format): > ## 🚀 Highlights > - **Fiesta Mode** — Discover and orchestrate across multiple PolyPilot instances on your LAN with push-to-pair (PureWeen#322) > - **Mixed-Model PR Review Squad** — New preset combining Opus, Sonnet, and Codex for comprehensive code review (PureWeen#451) > > ## ✨ New Features > - Surface CLI subagent and skill events directly in chat + `/agent` command (PureWeen#445) > *(categorized, concise, scannable...)* ## Testing This is a documentation-only skill file — no code changes, no tests needed. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What this PR does
Three improvements to Fiesta mode for the 'one PolyPilot to rule them all' multi-devbox scenario:
Feature A: NIC Selection Fix
Feature B: Pairing String (copy/paste)
Feature C: LAN Push-to-Pair
Race condition fixes